夹心攻击(Sandwich Attack)技术原理与防御策略

深度解析:夹心攻击(Sandwich Attack)技术原理与防御策略

夹心攻击(Sandwich Attack)作为去中心化交易所(DEX)场景下的一种典型MEV(Miner Extractable Value)攻击方式,近年来受到广泛关注。本文将从技术原理、实现机制、攻击收益来源、防御策略等多个维度进行深度解析,并给出审计实战中的风险识别方法。

一、夹心攻击的核心定义与关系澄清

1.1 一句话定义

夹心攻击是一种典型的DEX场景下的MEV攻击,攻击者通过抢跑(Front-running)和尾随(Back-running)两笔交易,将用户的交易“夹在中间”,从而将用户的滑点损失转化为自己的套利利润。

1.2 关键概念澄清

概念 是否完整描述夹心攻击 核心含义
Front-running ❌ 不完整 指“抢在前面执行”交易
Back-running ❌ 不完整 指“在后面执行”交易,吃结果
Sandwich Attack Front + Victim + Back 组合攻击
MEV ✅(更大集合) 区块生产者/机器人可提取的最大价值

关键结论:Sandwich Attack是MEV的一种具体实现方式。

二、攻击成立的前提条件

夹心攻击并非适用于所有交易场景,其成立依赖于以下条件:

2.1 DEX使用AMM模型

典型如Uniswap v2/v3、SushiSwap、PancakeSwap等,价格由池子储备决定,而非订单簿撮合。

2.2 用户交易特征

用户交易需满足以下条件之一:

这些特征是攻击者“确定能赚钱”的信号。

三、夹心攻击的完整机制拆解

3.1 Mempool:攻击的起点

用户交易进入公共内存池(Mempool)后,机器人实时监听关键信息,如swapExactTokensForTokensamountInminAmountOut及池子当前储备,并判断是否可以通过“先买、再让用户买、最后卖”的方式无风险套利。

3.2 攻击的三步结构

Tx₁: Attacker Buy   (Front-run)
Tx₂: Victim Buy     (Victim)
Tx₃: Attacker Sell  (Back-run)

3.3 每一步在AMM中的变化(以Uniswap v2模型为例)

初始状态

ReserveA = x
ReserveB = y
Price = y / x

🥪 Step 1:攻击者抢跑买入(抬价)

攻击者用TKA买入TKB,导致池子中A增加、B减少,B价格上涨。这一步的目的是人为制造更差的价格给受害者。

🥪 Step 2:受害者被迫高价成交

由于价格已被抬高,但minAmountOut仍然满足,受害者交易成功但成交价格极差。这一步制造了额外价格冲击和可被收割的滑点空间。

🥪 Step 3:攻击者砸盘套利

攻击者将Step 1买到的TKB卖回池子,导致B增加、A减少,价格部分回落。攻击者赚取的利润为受害者多付出的价格差减去手续费。

四、Python模拟:夹心攻击的教学级实现

4.1 模拟代码

def simulate_sandwich_attack(dex_contract, attacker, victim, amount_victim_in):
    print(f"🕵️ 机器人发现受害者准备用 {amount_victim_in} TKA 换取 TKB")

    # 初始储备
    resA_0 = dex_contract.functions.reserveA().call()
    resB_0 = dex_contract.functions.reserveB().call()
    print(f"初始池子: A={resA_0}, B={resB_0}")

    # --- Step 1: 抢跑 ---
    attacker_in = 200 * 10**18
    dex_contract.functions.swapAtoB(attacker_in).transact({'from': attacker})

    # --- Step 2: 受害者成交 ---
    dex_contract.functions.swapAtoB(amount_victim_in).transact({'from': victim})

    # --- Step 3: 攻击者卖出 ---
    attacker_b = token_b.functions.balanceOf(attacker).call()
    token_a_before = token_a.functions.balanceOf(attacker).call()
    dex_contract.functions.swapBtoA(attacker_b).transact({'from': attacker})
    token_a_after = token_a.functions.balanceOf(attacker).call()

    profit = token_a_after - token_a_before - attacker_in
    print(f"💰 攻击者净利润: {profit}")

4.2 模拟隐含的重要假设

  1. 假设攻击者一定能夹成功:实际中存在同区块竞争和更高手续费的机器人。
  2. 忽略了手续费:实际利润公式应为Profit ≈ Victim Slippage − 2 × Swap Fee − Gas
  3. 忽略v3的流动性分段:v3中sandwich更复杂,但依然存在。

五、攻击收益的本质来源

夹心攻击并非“偷钱”,而是“重分配”。攻击者赚取的是用户因滑点容忍度过高而在AMM曲线上多付出的部分。换句话说,攻击者把“用户对价格不敏感”的部分变现了。

六、防御手段(从弱到强)

6.1 严格设置minAmountOut

require(amountOut >= minAmountOut, "SLIPPAGE_TOO_HIGH");

效果:夹心第一步抬价后,受害者交易revert,攻击者被迫高位接盘。这是唯一能在协议层强制保护用户的方式。

6.2 私有交易通道(Flashbots/MEV-Share)

交易不进公共mempool,攻击者无法看见用户交易。缺点包括用户体验复杂和中心化信任假设。

6.3 提高流动性深度(经济防御)

TVL增加,同样的攻击成本上升,套利空间下降。大池子sandwich少,小池子是重灾区。

七、延伸思考

7.1 高级主题

7.2 一句话总结

夹心攻击不是漏洞,而是AMM、公共内存池和用户滑点容忍共同产生的必然结果。

如何在代码中识别Sandwich Attack风险(审计实战版)

一、从「入口函数」开始:最危险的函数长什么样?

1.1 高危函数签名

swap(...)
swapExactTokensForTokens(...)
swapExactETHForTokens(...)
swapTokensForExactTokens(...)

尤其是:

function swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
)

审计信号

二、第一类核心风险:滑点保护是否真实存在

2.1 完全没有滑点保护(极高危)

uint amountOut = getAmountOut(amountIn);
_transfer(to, amountOut);

结论:100% sandwich可行,攻击者可随意操纵价格。

2.2 滑点参数存在,但未使用(经典漏洞)

function swap(uint amountIn, uint minOut) external {
    uint out = getAmountOut(amountIn);
    _swap(out);
    // ❌ minOut没有任何校验
}

结论:非常常见的新手DEX错误。

2.3 滑点保护存在,但允许极端值

require(amountOut >= minOut, "SLIPPAGE");

minOut == 0允许或前端默认给一个极低值。
审计结论:合约没bug,但系统性sandwich高风险。

三、第二类风险:价格是否完全由可操纵状态决定

3.1 AMM价格公式是否直接依赖reserve?

price = reserveB / reserveA;

或:

amountOut = (amountIn * reserveOut) / (reserveIn + amountIn);

判断:如果reserve只受swap影响且swap可被插队,则是sandwich的必要条件。

3.2 使用即时价格,而非时间加权价格(TWAP)

uint price = getSpotPrice();

而不是:

uint price = getTWAP(30 minutes);

结论:spot price可被瞬时操纵。

四、第三类风险:交易是否“原子可预测”

Sandwich成立的另一个关键是攻击者能精确预测你的成交结果。

4.1 成交金额完全确定

amountOut = getAmountOut(amountIn);

无随机性、无延迟、无外部依赖。
结论:非常适合sandwich bot做精确模拟,攻击收益高度确定。

4.2 单区块内完成所有状态更新

swap();
updateReserve();

风险说明:同一区块内front + victim + back完全可组合。

五、第四类风险:交易是否暴露在公共mempool

5.1 未提供私有交易入口

审计语言

Protocol relies on public mempool execution, making it vulnerable to MEV-based sandwich attacks.

六、第五类风险:是否存在“隐式sandwich放大器”

这些不是必要条件,但会放大攻击收益。

6.1 自动帮用户兜底滑点(反模式)

if (amountOut < minOut) {
    minOut = amountOut * 95 / 100;
}

结论:这是反MEV的反面教材。

6.2 路由器自动拆单/聚合

swap(A → B → C)

路径越长,sandwich表面积越大。

6.3 针对大额交易无特殊处理

if (amountIn > threshold) {
    // ❌ 没有任何保护
}

七、审计时可直接用的「Sandwich风险检查表」

[ ] 是否存在swap/trade函数
[ ] 是否使用AMM即时价格
[ ] 是否依赖reserve状态定价
[ ] 是否有minAmountOut校验
[ ] minAmountOut是否允许为0
[ ] 是否使用spot price而非TWAP
[ ] 是否暴露在公共mempool
[ ] 是否对大额交易做特殊处理
[ ] 是否存在自动调整滑点逻辑

八、审计结论该怎么写

示例审计描述

The protocol’s swap mechanism relies on spot AMM pricing and user-configurable slippage parameters. As transactions are executed via the public mempool, they are susceptible to MEV-based sandwich attacks, especially for large trades with high slippage tolerance. This is an economic design risk rather than a correctness bug.